import functools
import io
import json
from base64 import b64decode

from django.conf import settings
from django.db import transaction
from django.db.models import Exists, OuterRef
from django.template.defaultfilters import filesizeformat
from django.utils import timezone
from rest_framework import serializers

from sysreptor.pentests.models import (
    ArchivedProject,
    ArchivedProjectKeyPart,
    ArchivedProjectPublicKeyEncryptedKeyPart,
    PentestProject,
    UserPublicKey,
)
from sysreptor.users.models import PentestUser
from sysreptor.users.serializers import PentestUserSerializer
from sysreptor.utils import crypto
from sysreptor.utils.configuration import configuration
from sysreptor.utils.crypto import CryptoError, pgp


class UserPublicKeySerializer(serializers.ModelSerializer):
    class Meta:
        model = UserPublicKey
        fields = ['id', 'created', 'updated', 'name', 'enabled', 'public_key', 'public_key_info']
        read_only_fields = ['public_key', 'public_key_info']


class UserPublicKeyRegisterBeginSerializer(UserPublicKeySerializer):
    class Meta(UserPublicKeySerializer.Meta):
        read_only_fields = ['public_key_info']

    def create(self, validated_data):
        try:
            public_key_info = pgp.public_key_info(validated_data['public_key'])
        except CryptoError as ex:
            raise serializers.ValidationError(detail=ex.args[0]) from ex

        return UserPublicKey(**validated_data | {
            'public_key_info': public_key_info,
        })


class ArchivedProjectKeyPartSerializer(serializers.ModelSerializer):
    user = PentestUserSerializer(read_only=True)

    class Meta:
        model = ArchivedProjectKeyPart
        fields = ['id', 'created', 'updated', 'user', 'is_decrypted', 'decrypted_at']


class ArchivedProjectSerializer(serializers.ModelSerializer):
    key_parts = ArchivedProjectKeyPartSerializer(many=True, read_only=True)
    reencrypt_key_parts_after_inactivity_date = serializers.SerializerMethodField()
    size = serializers.SerializerMethodField()

    class Meta:
        model = ArchivedProject
        fields = ['id', 'created', 'updated', 'auto_delete_date', 'reencrypt_key_parts_after_inactivity_date', 'name', 'tags', 'threshold', 'key_parts', 'size']

    def get_reencrypt_key_parts_after_inactivity_date(self, obj) -> str | None:
        decrypted_dates = [k.decrypted_at for k in obj.key_parts.all() if k.is_decrypted]
        if not decrypted_dates:
            return None
        return max(decrypted_dates) + settings.AUTOMATICALLY_RESET_STALE_ARCHIVE_RESTORES_AFTER

    def get_size(self, obj) -> str:
        if obj.file is None or obj.file.size is None:
            return '0 B'
        return filesizeformat(obj.file.size)


class ArchivedProjectPublicKeyEncryptedKeyPartSerializer(serializers.ModelSerializer):
    public_key = UserPublicKeySerializer(read_only=True)

    class Meta:
        model = ArchivedProjectPublicKeyEncryptedKeyPart
        fields = ['id', 'created', 'updated', 'public_key', 'encrypted_data']


class ArchivedProjectKeyPartDecryptSerializer(serializers.Serializer):
    data = serializers.CharField(write_only=True)

    status = serializers.ChoiceField(choices=['key-part-decrypted', 'project-restored'], read_only=True)
    project_id = serializers.UUIDField(read_only=True)

    def validate_data(self, value):
        try:
            return b64decode(value)
        except Exception as ex:
            raise serializers.ValidationError('Invalid format. Expected base64 encoded data') from ex

    def validate(self, attrs):
        if self.instance.is_decrypted:
            raise serializers.ValidationError('Already decrypted')
        return super().validate(attrs)

    def update(self, instance, validated_data):
        try:
            with crypto.open(io.BytesIO(instance.encrypted_key_part), mode='rb', key=crypto.EncryptionKey(id=None, key=validated_data['data'])) as c:
                instance.key_part = json.loads(c.read())
                instance.decrypted_at = timezone.now()
        except Exception as ex:
            raise serializers.ValidationError('Decryption of key part failed') from ex
        instance.save()
        output = {
            'status': 'key-part-decrypted',
            'project_id': None,
        }

        # Restore whole project when enough key parts are decrypted
        archive = self.context['archived_project']
        available_key_parts = list(archive.key_parts.exclude(decrypted_at=None))
        if len(available_key_parts) >= archive.threshold:
            project = ArchivedProject.objects.restore_project(archive)
            output |= {
                'status': 'project-restored',
                'project_id': project.id,
            }

        return output


class PentestProjectCreateArchiveSerializer(serializers.Serializer):
    @functools.cached_property
    def _get_archive_users(self):
        return ArchivedProject.objects.get_archive_users_for_project(self.context['project'])

    def get_archive_users(self):
        return self._get_archive_users

    def validate(self, attrs):
        if not self.context['project'].readonly:
            raise serializers.ValidationError('Cannot archive non-finished project')
        if len(self.get_archive_users()) < int(configuration.ARCHIVING_THRESHOLD):
            raise serializers.ValidationError('Too few users')
        return super().validate(attrs)

    @transaction.atomic()
    def create(self, validated_data):
        return ArchivedProject.objects.create_from_project(
            project=self.context['project'],
            users=self.get_archive_users(),
            delete_project=True,
        )


class PentestUserCheckArchiveSerializer(PentestUserSerializer):
    is_project_member = serializers.BooleanField()
    has_public_keys = serializers.BooleanField()
    can_restore = serializers.SerializerMethodField()
    has_permissions = serializers.SerializerMethodField()
    warnings = serializers.SerializerMethodField()

    class Meta(PentestUserSerializer.Meta):
        fields = PentestUserSerializer.Meta.fields + ['is_active', 'is_global_archiver', 'is_project_member', 'has_public_keys', 'has_permissions', 'can_restore', 'warnings']

    def get_has_permissions(self, obj) -> bool:
        return obj.is_global_archiver or (obj.is_project_member and configuration.PROJECT_MEMBERS_CAN_ARCHIVE_PROJECTS)

    def get_can_restore(self, obj) -> bool:
        return obj.is_active and obj.has_public_keys and self.get_has_permissions(obj)

    def get_warnings(self, obj) -> list[str]:
        warnings = []
        if not obj.is_active:
            warnings.append('User is not active')
        if not obj.has_public_keys:
            warnings.append('User has no public keys enabled')
        if not self.get_has_permissions(obj):
            warnings.append('User is not a global archiver')
        return warnings


class PentestProjectCheckArchiveSerializer(serializers.ModelSerializer):
    users = PentestUserCheckArchiveSerializer(many=True, read_only=True)

    class Meta:
        model = PentestProject
        fields = ['users']

    def update(self, instance, validated_data):
        return {
            'users': ArchivedProject.objects \
                .get_possible_archive_users_for_project(instance, always_include_members=True) \
                .annotate_has_public_keys() \
                .annotate(is_project_member=Exists(PentestUser.objects.filter(projectmemberinfo__project=instance).filter(pk=OuterRef('pk')))),
        }



